package com.zzyl.job;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.google.common.collect.Lists;
import com.zzyl.constant.CacheConstants;
import com.zzyl.dto.AlertNotifyDto;
import com.zzyl.entity.AlertData;
import com.zzyl.entity.AlertRule;
import com.zzyl.mapper.AlertDataMapper;
import com.zzyl.mapper.AlertRuleMapper;
import com.zzyl.mapper.DeviceMapper;
import com.zzyl.mapper.UserRoleMapper;
import com.zzyl.properties.AlertInfoProperties;
import com.zzyl.vo.DeviceDataVo;
import com.zzyl.ws.WebSocketServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 设备上报数据报警规则过滤
 */
@Slf4j
@Component
public class AlertJob {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Autowired
    private AlertRuleMapper alertRuleMapper;
    @Autowired
    private DeviceMapper deviceMapper;
    @Autowired
    private AlertInfoProperties alertInfoProperties;
    @Autowired
    private UserRoleMapper userRoleMapper;
    @Autowired
    private AlertDataMapper alertDataMapper;
    @Autowired
    private WebSocketServer webSocketServer;

    @Scheduled(cron = "0/20 * * * * ? ")
    public void deviceDataAlertFilter() {
        //1.查询所有报警规则，如果为空，程序结束
        List<AlertRule> allAlertRules = alertRuleMapper.selectAll();
        if (CollUtil.isEmpty(allAlertRules)) {
            return;
        }

        //2.查询所有设备最新上报数据，如果为空，程序结束
        List<Object> jsonStrList = stringRedisTemplate.opsForHash().values(CacheConstants.IOT_DEVICE_LAST_DATA);
        if (CollUtil.isEmpty(jsonStrList)) {
            return;
        }

        //3.设备上报数据不为空，提取设备上报数据为list
        List<DeviceDataVo> deviceDataVoList = new ArrayList<>();

        jsonStrList.forEach(json -> deviceDataVoList.addAll(JSONUtil.toList(json.toString(), DeviceDataVo.class)));

        //4.对每一条设备上报数据进行报警规则校验
        deviceDataVoList.forEach(this::alertFilter);
    }

    private void alertFilter(DeviceDataVo d) {
        //获取设备上报数据时间，如果上报发生在1分钟前，为历史数据，不再处理
        LocalDateTime alarmTime = d.getAlarmTime();
        long between = LocalDateTimeUtil.between(alarmTime, LocalDateTime.now(), ChronoUnit.SECONDS);
        if (between > 60) {
            return;
        }

        //查询报警规则，将对于全部设备的报警规则 和 对于本设备的报警规则合并
        List<AlertRule> allDeviceAlertRules = alertRuleMapper.selectByFunctionId(d.getFunctionId(), "-1", d.getProductKey());
        List<AlertRule> theDeviceAlertRules = alertRuleMapper.selectByFunctionId(d.getFunctionId(), d.getIotId(), d.getProductKey());
        Collection<AlertRule> alertRules = CollUtil.addAll(allDeviceAlertRules, theDeviceAlertRules);

        //如果报警规则为空，程序结束
        if (CollectionUtil.isEmpty(alertRules)) {
            return;
        }

        //如果报警规则不为空，遍历报警规则，进行校验
        alertRules.forEach(alertRule -> deviceDateAlarmHandler(d, alertRule));
    }

    private void deviceDateAlarmHandler(DeviceDataVo d, AlertRule alertRule) {

        String[] aepArr = alertRule.getAlertEffectivePeriod().split("~");
        LocalTime startTime = LocalTime.parse(aepArr[0]);
        LocalTime endTime = LocalTime.parse(aepArr[1]);
        //上报时间
        LocalTime time = LocalDateTimeUtil.of(d.getAlarmTime()).toLocalTime();

        //如果上报时间不在生效时间段内，校验结束
        if (time.isBefore(startTime) || time.isAfter(endTime)) {
            return;
        }

        //第二层校验：将设备上报值和阈值比较
        //x:第一个值，y:第二个值。x==y返回0，x<y返回小于0的数，x>y返回大于0的数
        int compareResult = Double.compare(Double.parseDouble(d.getDataValue()), alertRule.getValue());
        //或 逻辑运算，2个条件满足其一即为true
        //1.运算符为>=，且设备上报值 >= 阈值
        //2.运算符为<，且设备上报值 < 阈值
        if ((ObjectUtil.equals(alertRule.getOperator(), ">=") && compareResult >= 0)
                || (ObjectUtil.equals(alertRule.getOperator(), "<=") && compareResult <= 0)) {
            log.debug("此时设备上报数据达到阈值");
        } else {
            return;
        }
        //第三层校验：校验当前设备+功能的报警规则是否处于沉默周期
        String silentCycleCacheKey = CacheConstants.ALERT_SILENT_CYCLE +
                d.getIotId() + ":" + d.getFunctionId() + ":" + alertRule.getId();
        String silentCycleData = stringRedisTemplate.opsForValue().get(silentCycleCacheKey);
        //如果redis中存在对应沉默周期数据，则当前设备+功能正处于沉默周期，无需报警
        if (StrUtil.isNotEmpty(silentCycleData)) {
            return;
        }

        //第四层校验：校验持续周期


        //从redis中获取数据，报警规则连续触发次数
        String triggerCountCacheKey = CacheConstants.ALERT_TRIGGER_COUNT +
                d.getIotId() + ":" + d.getFunctionId() + ":" + alertRule.getId();
        /////////////////////////////
        //4.1获取连续触发报警规则的次数
        //aggCountData是上次触发报警累计到的次数，这一次又触发了报警，所以累积到这次就+1
        String triggerCountData = stringRedisTemplate.opsForValue().get(triggerCountCacheKey);
        Integer triggerCount = ObjectUtil.isEmpty(triggerCountData) ? 1 : Integer.parseInt(triggerCountData) + 1;

        //4.2判断次数是否与持续周期相等
        //如果触发报警规则次数不等于持续周期，则无需报警
        if (ObjectUtil.notEqual(alertRule.getDuration(), triggerCount)) {
            stringRedisTemplate.opsForValue().set(triggerCountCacheKey, triggerCount.toString());
            return;
        }

        //如果触发报警规则次数等于持续周期，则需要报警，完成以下操作：
        //1）删除报警规则连续触发次数的缓存
        //2）添加对应的沉默周期，设置过期时间添加对应的沉默周期，设置过期时间
        //3）报警数据存储到数据库
        stringRedisTemplate.delete(triggerCountCacheKey);
        stringRedisTemplate.opsForValue().set(silentCycleCacheKey,
                d.getDataValue(), alertRule.getAlertSilentPeriod(), TimeUnit.MINUTES);

        //获取消息的消费者
        List<Long> consumerIds = Lists.newArrayList();

        if (ObjectUtil.equals(0, alertRule.getAlertDataType())) {//0老人异常数据
            //如果是老人报警数据，需要通知绑定老人的护理员。根据iotId查询老人绑定的护理员
            if (d.getLocationType() == 0) {//0随身设备 1固定设备
                consumerIds = deviceMapper.selectNursingIdsByIotIdWithElder(d.getIotId());
            } else if (d.getLocationType() == 1 && d.getPhysicalLocationType() == 2) {
                consumerIds = deviceMapper.selectNursingIdsByIotIdWithBed(d.getIotId());
            }
        } else {//1设备异常数据
            //如果是设备报警数据，需要通知设备维护人员。根据指定角色名称查询相关用户
            consumerIds = userRoleMapper.selectUserIdsByRoleName(alertInfoProperties.getDeviceMaintainerRole());
        }

        //查询超级管理员，超级管理员无论什么消息都会接收
        List<Long> managerIds = userRoleMapper.selectUserIdsByRoleName("超级管理员");
        List<Long> allConsumerIds = CollUtil.addAllIfNotContains(consumerIds, managerIds);
        allConsumerIds = CollUtil.distinct(allConsumerIds);

        //新增报警数据
        insertAlertData(d, alertRule, allConsumerIds);
        //发送websocket消息
        websocketNotify(d, alertRule, allConsumerIds);
    }

    private void websocketNotify(DeviceDataVo d, AlertRule alertRule, List<Long> allConsumerIds) {
        AlertNotifyDto dto = AlertNotifyDto.builder().build();
        BeanUtil.copyProperties(alertRule, dto);
        BeanUtil.copyProperties(d, dto);
        dto.setIsAllConsumer(false);
        dto.setNotifyType(1);
        dto.setVoiceNotifyStatus(1);
        //给指定消费者发送消息
        webSocketServer.sendMessageToConsumer(dto, allConsumerIds);
    }


    private void insertAlertData(DeviceDataVo d, AlertRule alertRule, List<Long> allConsumerIds) {

        String alertReason = StrUtil.format("{} {} {}, 持续 {} 个周期就报警",
                alertRule.getFunctionName(),
                alertRule.getOperator(),
                alertRule.getValue(),
                alertRule.getDuration());

        AlertData alertData = BeanUtil.toBean(d, AlertData.class);
        alertData.setAlertRuleId(alertRule.getId());// 报警规则id
        alertData.setAlertReason(alertReason);// 报警原因，格式：功能名称+运算符+阈值+持续周期+聚合周期
        alertData.setType(alertRule.getAlertDataType());//报警数据类型，0：老人异常数据，1：设备异常数据
        alertData.setStatus(0);// 状态，0：待处理，1：已处理

        List<AlertData> list = allConsumerIds.stream().map(id -> {
            AlertData dbAlertData = BeanUtil.toBean(alertData, AlertData.class);
            dbAlertData.setUserId(id);// 接收者id
            return dbAlertData;
        }).collect(Collectors.toList());

        alertDataMapper.batchInsert(list);

    }
}
